import argparse
import csv
import os
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.ticker import MaxNLocator
import numpy as np
from datetime import datetime
import re
import textwrap

# 设置全局字体为支持中文的字体(如 SimHei、Microsoft YaHei)
plt.rcParams['font.sans-serif'] = ['SimHei']  # Windows 系统常用字体
# plt.rcParams['font.sans-serif'] = ['Arial Unicode MS']  # macOS 系统常用字体
plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题

args = None

# 定义高对比度颜色调色板
color_list = [
'#FF0000',  # 鲜红色
'#0000FF',  # 鲜蓝色
'#800080',  # 紫红色
'#FF8000',  # 橙色
'#8000FF',  # 紫色
'#FF0080',  # 玫红色
'#80FF00',  # 黄绿色
'#0080FF',  # 天蓝色
'#FF8080',  # 浅红色
'#800000',  # 深红色
'#008000',  # 深绿色
'#000080',  # 深蓝色
'#808000',  # 橄榄色
'#008080',  # 深青色
'#FFA500',  # 金橙色
'#32CD32',  # 石灰绿
'#FF1493',  # 深粉色
'#1E90FF',  # 道奇蓝
]

def convert_value(value):
    """转换百分比和带逗号的数字为浮点数"""
    if isinstance(value, str):
        value = value.strip()
        if value.endswith('%'):
            try:
                return float(value.strip('%').strip()) / 100.0
            except ValueError:
                print(f"警告: 无法转换百分比值: {value}")
                return 0.0
        elif ',' in value:
            try:
                return float(value.replace(',', ''))
            except ValueError:
                print(f"警告: 无法转换带逗号的数值: {value}")
                return 0.0
        else:
            try:
                return float(value)
            except ValueError:
                print(f"警告: 无法转换数值: {value}")
                return 0.0

    try:
        return float(value)
    except (ValueError, TypeError):
        print(f"警告: 数据类型转换失败: {value}")
        return 0.0

def parse_labels(labels_args):
    """解析标签参数 - 支持 field:label 格式"""
    if not labels_args:
        return {}

    labels_dict = {}

    for arg in labels_args:
        if ':' in arg:
            parts = arg.split(':', 1)  # 只分割成2部分，避免标签中的冒号被分割
            if len(parts) == 2:
                field = parts[0].strip()
                label = parts[1].strip()
                labels_dict[field] = label
                print(f"字段 '{field}' 标签设置为: '{label}'")
            else:
                print(f"警告: 标签参数格式错误: {arg}")
        else:
            print(f"警告: 标签参数格式错误,应包含冒号: {arg}")

    return labels_dict

def parse_hlines(hlines_args, y_fields):
    """解析水平线参数 - 支持 field:value:color:desc 格式，其中field和value必填，color和desc可选"""
    if not hlines_args:
        return {}

    hlines_dict = {}

    # 颜色正则表达式 - 匹配十六进制颜色
    color_pattern = re.compile(r'^#[0-9A-Fa-f]{6}$')

    for arg in hlines_args:
        if ':' in arg:
            parts = arg.split(':', 3)  # 最多分割成4部分

            if len(parts) >= 2:
                field = parts[0].strip()

                # 检查字段是否在y_fields中
                if field not in y_fields:
                    print(f"警告: 水平线字段 '{field}' 不在Y轴字段列表中,将被忽略")
                    continue

                try:
                    # field和value是必填的
                    value = float(parts[1].strip())
                    color = '#ff0000'
                    desc = ''

                    # 处理可选的第三和第四个参数
                    if len(parts) >= 3:
                        third_part = parts[2].strip()
                        if third_part.startswith('#') and color_pattern.match(third_part):
                            # 第三个参数是颜色
                            color = third_part
                            # 检查是否有第四个参数作为描述
                            if len(parts) == 4:
                                desc = parts[3].strip().strip('"').strip("'")
                        else:
                            # 第三个参数不是颜色，当作描述处理
                            desc = third_part.strip('"').strip("'")

                    # 将数据添加到字典中
                    if field not in hlines_dict:
                        hlines_dict[field] = []
                    hlines_dict[field].append((value, color, desc))

                    # 输出信息
                    info_parts = [f"为字段 '{field}' 添加水平线: {value}"]
                    if color:
                        info_parts.append(f"颜色: {color}")
                    if desc:
                        info_parts.append(f"名称: {desc}")
                    print(" (".join(info_parts) + (")" if len(info_parts) > 1 else ""))

                except ValueError as e:
                    print(f"警告: 无法解析水平线值: {arg}, 错误: {e}")
            else:
                print(f"警告: 水平线参数格式错误: {arg}")
        else:
            print(f"警告: 水平线参数格式错误,应包含冒号: {arg}")

    return hlines_dict

def parse_xlim(xlim_arg, xf_format=None):
    """解析X轴范围参数 - 支持数字和日期格式"""
    if not xlim_arg:
        return None
    
    if ':' not in xlim_arg:
        print(f"警告: X轴范围参数格式错误,应包含冒号: {xlim_arg}")
        return None
    
    parts = xlim_arg.split(':', 1)  # 只分割成2部分
    if len(parts) != 2:
        print(f"警告: X轴范围参数格式错误: {xlim_arg}")
        return None
    
    start_str = parts[0].strip()
    end_str = parts[1].strip()
    
    try:
        if xf_format:
            # 日期格式解析
            start_val = datetime.strptime(start_str, xf_format)
            end_val = datetime.strptime(end_str, xf_format)
            print(f"X轴范围设置为: {start_str} 到 {end_str} (日期格式)")
        else:
            # 数字格式解析
            start_val = float(start_str)
            end_val = float(end_str)
            print(f"X轴范围设置为: {start_val} 到 {end_val}")
        
        return (start_val, end_val)
    
    except ValueError as e:
        if xf_format:
            print(f"警告: X轴日期范围解析失败: {xlim_arg}, 日期格式: {xf_format}, 错误: {e}")
        else:
            print(f"警告: X轴数字范围解析失败: {xlim_arg}, 错误: {e}")
        return None

def parse_ylim_new(ylim_args, y_fields):
    """解析Y轴范围参数 - 新格式 field:start:end"""
    if not ylim_args:
        return {}
    
    ylim_dict = {}
    
    for arg in ylim_args:
        if ':' not in arg:
            print(f"警告: Y轴范围参数格式错误,应包含冒号: {arg}")
            continue
        
        parts = arg.split(':', 2)  # 分割成最多3部分
        if len(parts) != 3:
            print(f"警告: Y轴范围参数格式错误,应为 field:start:end 格式: {arg}")
            continue
        
        field = parts[0].strip()
        start_str = parts[1].strip()
        end_str = parts[2].strip()
        
        # 检查字段是否在y_fields中
        if field not in y_fields:
            print(f"警告: Y轴范围字段 '{field}' 不在Y轴字段列表中,将被忽略")
            continue
        
        try:
            start_val = float(start_str)
            end_val = float(end_str)
            ylim_dict[field] = (start_val, end_val)
            print(f"字段 '{field}' Y轴范围设置为: {start_val} 到 {end_val}")
        except ValueError:
            print(f"警告: Y轴范围参数数值解析失败: {arg}")
    
    return ylim_dict

def apply_axis_limits_new(ax, xlim=None, ylim_value=None, field_name="", is_main_ax=False):
    """应用坐标轴范围限制 - 改进版"""
    # 设置X轴范围（只在主轴上设置）
    if xlim is not None and is_main_ax:
        if isinstance(xlim, (tuple, list)) and len(xlim) == 2:
            ax.set_xlim(xlim[0], xlim[1])
            # 根据xlim的类型判断是日期还是数字
            if isinstance(xlim[0], datetime):
                print(f"X轴范围已应用: {xlim[0].strftime('%Y.%m.%d_%H.%M.%S')} 到 {xlim[1].strftime('%Y.%m.%d_%H.%M.%S')}")
            else:
                print(f"X轴范围已应用: {xlim[0]} 到 {xlim[1]}")
        else:
            print("警告: xlim 应该是包含两个数值的元组或列表")
    
    # 设置Y轴范围
    if ylim_value is not None:
        if isinstance(ylim_value, (tuple, list)) and len(ylim_value) == 2:
            ax.set_ylim(ylim_value[0], ylim_value[1])
            if field_name:
                print(f"字段 '{field_name}' Y轴范围已应用: {ylim_value[0]} 到 {ylim_value[1]}")
        else:
            print(f"警告: 字段 '{field_name}' 的ylim值格式错误")

def parse_notes(notes_args):
    """解析节点标注参数 - 支持缺省模式和完整模式，支持换行符"""
    if not notes_args:
        return {}

    notes_dict = {
        'lt': [],  # 左上
        'rt': [],  # 右上
        'rb': [],  # 右下
        'lb': [],  # 左下
        'lc': [],  # 左中
        'rc': [],  # 右中
        'ct': [],  # 中上
        'cb': []   # 中下
    }

    # 颜色正则表达式 - 匹配十六进制颜色
    color_pattern = re.compile(r'^#[0-9A-Fa-f]{6}$')

    for arg in notes_args:
        if ':' in arg:
            # 完整模式: position:color:"message" 或 position:"message"
            parts = arg.split(':', 2)

            if len(parts) >= 2:
                position = parts[0].strip().lower()

                # 验证位置参数 - 增加新的位置选项
                if position not in ['lt', 'rt', 'rb', 'lb', 'lc', 'rc', 'ct', 'cb']:
                    print(f"警告: 无效的节点位置 '{position}', 支持的位置: lt, rt, rb, lb, lc, rc, ct, cb")
                    continue

                if len(parts) == 2:
                    # 格式: position:"message"
                    message = parts[1].strip().strip('"').strip("'")
                    color = None
                elif len(parts) == 3:
                    # 格式: position:color:"message"
                    color_or_message = parts[1].strip()
                    message = parts[2].strip().strip('"').strip("'")

                    # 判断第二部分是颜色还是消息
                    if color_pattern.match(color_or_message):
                        color = color_or_message
                    else:
                        # 如果不是颜色格式，则认为是消息的一部分
                        message = f"{color_or_message}:{message}"
                        color = None

                # 处理换行符
                message = message.replace('\\n', '\n')

                notes_dict[position].append({
                    'message': message,
                    'color': color
                })

                # 修复：将反斜杠字符串提取到变量中
                display_message = message.replace('\n', '\\n')
                if color:
                    print(f"为位置 '{position}' 添加节点: {display_message} (颜色: {color})")
                else:
                    print(f"为位置 '{position}' 添加节点: {display_message}")
        else:
            # 缺省模式: "message"
            message = arg.strip().strip('"').strip("'")
            # 处理换行符
            message = message.replace('\\n', '\n')
            # 默认添加到左上角
            notes_dict['lt'].append({
                'message': message,
                'color': None
            })
            # 修复：将反斜杠字符串提取到变量中
            display_message = message.replace('\n', '\\n')
            print(f"为位置 'lt'(默认) 添加节点: {display_message}")

    return notes_dict

def get_legend_width(fig, legend_bbox=None):
    """获取图例的宽度（以figure坐标为单位）"""
    if legend_bbox is None:
        # 默认图例位置和大小估算
        return 0.15  # 默认15%的figure宽度

    # 如果有具体的图例bbox信息，可以计算实际宽度
    # 这里简化处理，返回一个合理的估计值
    return 0.15

def wrap_text_to_width(text, max_chars_per_line=20):
    """将文本按指定字符数换行"""
    if '\n' in text:
        # 如果已经有换行符，分别处理每行
        lines = text.split('\n')
        wrapped_lines = []
        for line in lines:
            if len(line) <= max_chars_per_line:
                wrapped_lines.append(line)
            else:
                # 使用textwrap进行智能换行
                wrapped = textwrap.fill(line, width=max_chars_per_line, break_long_words=True, break_on_hyphens=True)
                wrapped_lines.append(wrapped)
        return '\n'.join(wrapped_lines)
    else:
        # 没有换行符的情况
        if len(text) <= max_chars_per_line:
            return text
        else:
            return textwrap.fill(text, width=max_chars_per_line, break_long_words=True, break_on_hyphens=True)

def add_horizontal_lines(ax, field, hlines_dict):
    """为指定轴添加水平线和说明文字"""
    if field in hlines_dict:
        for i, (value, color, desc) in enumerate(hlines_dict[field]):

            # 绘制水平线
            line = ax.axhline(y=value, color=color, linestyle='-',
                            linewidth=1.0, alpha=0.5)

            # 只有当有名称时才添加文字标注
            if desc:
                # 获取x轴范围来确定文字位置
                xlim = ax.get_xlim()
                # 在右侧
                x_pos = xlim[0] + (xlim[1] - xlim[0])
                # 添加文字标注,背景为白色半透明
                ax.text(x_pos, value, f'{desc}({value})',
                       verticalalignment='bottom',
                       horizontalalignment='right',
                       fontsize=9,
                       color=color,
                       bbox=dict(boxstyle='round,pad=0.3',
                                facecolor='white',
                                alpha=0.5,
                                edgecolor='red',
                                linewidth=0.5))

def add_node_annotations(fig, notes_dict, legend_width=0.15):
    """为整个图形添加节点标注 - 使用figure坐标系，支持换行和宽度限制"""
    if not notes_dict:
        return

    # 根据图例宽度计算节点文本的最大字符数
    # 假设每个字符占用大约0.008个figure宽度单位（这个值可能需要根据字体调整）
    char_width_ratio = 0.008
    max_chars_per_line = int(legend_width / char_width_ratio)
    # 设置合理的范围
    max_chars_per_line = max(10, min(max_chars_per_line, 30))

    # 定义位置映射 - 使用figure坐标系，增加center位置
    position_map = {
        'lt': (0.02, 0.98, 'left', 'top'),      # 左上
        'rt': (0.87, 0.98, 'left', 'top'),     # 右上
        'rb': (0.87, 0.02, 'left', 'bottom'),  # 右下
        'lb': (0.02, 0.02, 'left', 'bottom'),   # 左下
        'lc': (0.02, 0.50, 'left', 'center'),   # 左中
        'rc': (0.87, 0.50, 'left', 'center'),  # 右中
        'ct': (0.50, 0.98, 'center', 'top'),    # 中上
        'cb': (0.50, 0.02, 'center', 'bottom')  # 中下
    }

    for position, annotations in notes_dict.items():
        if not annotations:
            continue

        x_ratio, y_ratio, ha, va = position_map[position]

        for i, annotation in enumerate(annotations):
            message = annotation['message']
            color = annotation['color'] if annotation['color'] else 'black'

            # 对文本进行换行处理
            wrapped_message = wrap_text_to_width(message, max_chars_per_line)

            # 计算垂直偏移，避免重叠
            # 根据换行数量调整偏移
            line_count = wrapped_message.count('\n') + 1
            base_offset = 0.04 * line_count  # 每行占用的基础高度

            # 根据位置调整偏移方向
            if position in ['lt', 'rt', 'ct']:  # 上方位置
                y_offset = -base_offset * i
            elif position in ['lb', 'rb', 'cb']:  # 下方位置
                y_offset = base_offset * i
            else:  # 中间位置 (lc, rc)
                # 中间位置的多个标注可以向上下分布
                if i % 2 == 0:
                    y_offset = base_offset * (i // 2)
                else:
                    y_offset = -base_offset * ((i + 1) // 2)

            # 添加文字标注 - 使用figure坐标系
            fig.text(x_ratio, y_ratio + y_offset, wrapped_message,
                    verticalalignment=va,
                    horizontalalignment=ha,
                    fontsize=9,  # 稍微减小字体以适应更多文本
                    color=color,
                    bbox=dict(boxstyle='round,pad=0.4',
                             facecolor='white',
                             alpha=0.9,
                             edgecolor=color,
                             linewidth=1),
                    transform=fig.transFigure,  # 明确使用figure坐标系
                    linespacing=1.2)  # 设置行间距

def parse_csv(filename, x_field, y_fields):
    """解析CSV文件并提取数据"""
    x_data = []
    y_data = {field: [] for field in y_fields}

    with open(filename, 'r', encoding='utf-8') as f:
        reader = csv.DictReader(f)
        # 检查字段是否存在
        missing_fields = [field for field in y_fields if field not in reader.fieldnames]
        if missing_fields:
            raise ValueError(f"Y字段缺失: {', '.join(missing_fields)}")

        # 读取并转换数据
        count = 0
        for row in reader:
            x_val = count if not x_field else row[x_field]
            x_data.append(x_val)
            count += 1
            for field in y_fields:
                val = convert_value(row[field])
                y_data[field].append(val)

    return x_data, y_data

def get_x_range(all_x_data, args):
    """获取所有数据文件中x轴的范围"""
    if args.xf:
        try:
            all_dates = []
            dates = [datetime.strptime(str(d), args.xf) for d in all_x_data]
            all_dates.extend(dates)
            return min(all_dates), max(all_dates)
        except Exception as e:
            print(f"日期转换错误: {e}")
            return None, None
    else:
        all_values = []
        values = [float(x) for x in all_x_data]
        all_values.extend(values)
        return min(all_values), max(all_values)

def plot_single_file(args):
    """单文件绘图模式"""
    # 解析CSV数据
    y_fields = args.y
    try:
        x_data, y_data = parse_csv(args.inputs[0], args.x, y_fields)
    except Exception as e:
        print(f"错误: {e}")
        return

    # 解析水平线参数
    hlines_dict = parse_hlines(args.hlines, y_fields)

    # 解析节点参数
    notes_dict = parse_notes(args.notes)

    # 解析标签参数
    labels_dict = parse_labels(args.labels)

    # 解析X轴范围参数
    xlim_parsed = parse_xlim(args.xlim, args.xf)

    # 解析Y轴范围参数
    ylim_dict = parse_ylim_new(args.ylim, y_fields)

    # 创建图形和主坐标轴
    fig, ax_main = plt.subplots(figsize=(12, 6))

    # 转换横坐标格式(如果指定了格式)
    if args.xd:
        ax_main.xaxis.set_major_formatter(mdates.DateFormatter(args.xd))
    if args.xf:
        try:
            x_plot = [datetime.strptime(str(d), args.xf) for d in x_data]
        except Exception as e:
            print(f"横坐标转换错误: {e}")
            x_plot = x_data
    else:
        x_plot = x_data

    # 创建多个Y轴
    axes = [ax_main]  # 第一个轴是主轴

    # 为每个字段创建对应的Y轴(除了第一个)
    for i in range(1, len(y_fields)):
        ax = ax_main.twinx()
        # 调整右侧轴的位置,避免重叠
        if i > 1:
            ax.spines['right'].set_position(('outward', 60 * (i - 1)))
        axes.append(ax)

    color_limit = min(len(color_list), len(args.colors)) if args.colors else len(color_list)
    # 绘制每条曲线
    for i, field in enumerate(y_fields):
        ax = axes[i]
        if args.colors:
            color = color_list[args.colors[i % color_limit]]
        else:
            color = color_list[i % color_limit]
        # 绘制数据线(实线)
        ax.plot(x_plot, y_data[field], label=field, linestyle='-',
               marker='o', color=color, linewidth=1.0)

        # 设置Y轴标签和颜色 - 使用自定义标签或字段名
        ylabel = labels_dict.get(field, field)
        ax.set_ylabel(ylabel, fontsize=12, color=color)
        ax.tick_params(axis='y', labelcolor=color)

        # 设置Y轴spine颜色
        if i == 0:
            ax.spines['left'].set_color(color)
        else:
            ax.spines['right'].set_color(color)

    # 在所有数据线绘制完成后添加水平线和应用坐标轴范围限制
    for i, field in enumerate(y_fields):
        ax = axes[i]
        add_horizontal_lines(ax, field, hlines_dict)
        
        # 应用坐标轴范围限制
        is_main = (i == 0)  # 只有第一个轴是主轴
        ylim_value = ylim_dict.get(field)
        apply_axis_limits_new(ax, xlim=xlim_parsed, ylim_value=ylim_value, 
                             field_name=field, is_main_ax=is_main)

    # 设置X轴标签 - 使用自定义标签或字段名
    xlabel = labels_dict.get(args.x, args.x if args.x else 'Index')
    ax_main.set_xlabel(xlabel, fontsize=12)

    # 改进的横坐标刻度设置
    x_data_unique_length = len(set(x_plot))

    # 对于日期时间数据,使用智能刻度定位器
    if x_data_unique_length <= 10:
        # 数据点少时,显示所有点
        ax_main.xaxis.set_major_locator(MaxNLocator(nbins=x_data_unique_length, integer=True))
    else:
        # 大量数据,限制刻度数量
        ax_main.xaxis.set_major_locator(MaxNLocator(nbins=min(30, x_data_unique_length), integer=True))

    # 添加图例(只包含数据线,不包含水平线)
    lines = []
    labels = []
    legend_width = 0.2  # 单文件模式默认图例宽度

    for i, ax in enumerate(axes):
        # 只获取数据线的图例
        line_objects = ax.get_lines()
        for line in line_objects:
            if line.get_label() and not line.get_label().startswith('_'):
                # 排除水平线(axhline生成的线没有有效标签)
                if '=' not in line.get_label():
                    lines.append(line)
                    labels.append(line.get_label())

    if lines and labels:
        legend = ax_main.legend(lines, labels, loc='upper left')
        # 估算图例宽度（这里使用简单估算）
        legend_width = min(0.25, max(0.15, len(max(labels, key=len)) * 0.01))

    # 网格线(只在主轴上)
    ax_main.grid(True, linestyle='--', alpha=0.7)
    plt.xticks(rotation=45)

    # 设置标题
    if args.title:
        title = args.title
    else:
        title = '&'.join(y_fields)
        if args.flags and len(args.flags) > 0:
            title = f"{args.flags[0]}-{title}"
    plt.title(title, fontsize=14)

    # 在整个图形上添加节点标注（只显示一次）
    add_node_annotations(fig, notes_dict, legend_width)

    # 处理输出
    save_plot(fig, title, args)

def plot_multiple_files(args):
    """多文件绘图模式 - 图例显示在右侧外部"""
    inputs = args.inputs
    flags = args.flags if args.flags else [f"File{i+1}" for i in range(len(inputs))]
    y_fields = args.y

    # 确保flags和inputs数量一致
    if len(flags) != len(inputs):
        print(f"错误: flags数量({len(flags)})与inputs数量({len(inputs)})不匹配")
        return

    # 解析水平线参数
    hlines_dict = parse_hlines(args.hlines, y_fields)

    # 解析节点参数
    notes_dict = parse_notes(args.notes)

    # 解析标签参数
    labels_dict = parse_labels(args.labels)

    # 解析X轴范围参数
    xlim_parsed = parse_xlim(args.xlim, args.xf)

    # 解析Y轴范围参数
    ylim_dict = parse_ylim_new(args.ylim, y_fields)

    # 解析所有CSV文件数据
    all_data = []
    all_x_data = []

    for filename in inputs:
        try:
            x_data, y_data = parse_csv(filename, args.x, y_fields)
            all_data.append((x_data, y_data))
            all_x_data.append(x_data)
        except Exception as e:
            print(f"解析文件 {filename} 时出错: {e}")
            return

    # 创建子图 - 增加图形宽度为图例预留空间
    n_subplots = len(y_fields)
    fig, axes = plt.subplots(n_subplots, 1, figsize=(15, 4 * n_subplots))

    # 如果只有一个子图,确保axes是列表
    if n_subplots == 1:
        axes = [axes]

    # 简化线型样式
    if args.line_styles:
        print(f'Using custom line styles: {args.line_styles}')
        line_styles = args.line_styles
    else:
        line_styles = ['solid', 'dashed']
    # 简化标记样式
    markers = ['o', 's', '^', 'X']

    # 收集所有图例信息
    all_legend_handles = []
    all_legend_labels = []

    color_limit = min(len(color_list), len(args.colors)) if args.colors else len(color_list)
    # 为每个y字段创建一个子图
    for field_idx, field in enumerate(y_fields):
        ax = axes[field_idx]

        # 绘制每个文件的数据
        for file_idx, (flag, (x_data, y_data)) in enumerate(zip(flags, all_data)):
            if args.colors:
                color = color_list[args.colors[file_idx % color_limit]]
            else:
                color = color_list[file_idx % color_limit]
            style = line_styles[file_idx % len(line_styles)]
            marker = markers[file_idx % len(markers)]

            # 转换横坐标格式
            if args.xf:
                try:
                    x_plot = [datetime.strptime(str(d), args.xf) for d in x_data]
                except Exception as e:
                    print(f"横坐标转换错误: {e}")
                    x_plot = x_data
            else:
                x_plot = x_data

            # 绘制数据线
            label = f"{flag}-{field}"
            line = ax.plot(x_plot, y_data[field],
                          label=label,
                          linestyle=style,
                          marker=marker,
                          color=color,
                          linewidth=1.5,
                          markersize=6,
                          markerfacecolor=color,
                          markeredgecolor='white',
                          markeredgewidth=1.5,
                          alpha=0.8)

            # 只在第一个子图收集图例信息(避免重复)
            if field_idx == 0:
                all_legend_handles.extend(line)
                all_legend_labels.append(flag)

        # 为当前字段添加水平线和应用坐标轴范围限制
        add_horizontal_lines(ax, field, hlines_dict)
        
        # 应用坐标轴范围限制
        ylim_value = ylim_dict.get(field)
        apply_axis_limits_new(ax, xlim=xlim_parsed, ylim_value=ylim_value, 
                             field_name=field, is_main_ax=True)

        # 设置Y轴标签 - 使用自定义标签或字段名
        ylabel = labels_dict.get(field, field)
        ax.set_ylabel(ylabel, fontsize=12, fontweight='bold')

        # 设置x轴格式和标签 - 使用自定义标签或字段名
        if args.xd:
            ax.xaxis.set_major_formatter(mdates.DateFormatter(args.xd))

        xlabel = labels_dict.get(args.x, args.x if args.x else 'Index')
        ax.set_xlabel(xlabel, fontsize=12, fontweight='bold')

        # 横坐标刻度设置
        current_x_data = all_x_data[0] if all_x_data else []
        x_data_unique_length = len(set(current_x_data))

        if x_data_unique_length <= 10:
            ax.xaxis.set_major_locator(MaxNLocator(nbins=x_data_unique_length, integer=True))
        else:
            ax.xaxis.set_major_locator(MaxNLocator(nbins=min(30, x_data_unique_length), integer=True))

        # 网格线
        ax.grid(True, linestyle=':', alpha=0.4, linewidth=0.5)

        # 设置坐标轴样式
        ax.spines['top'].set_visible(False)
        ax.spines['right'].set_visible(False)
        ax.spines['left'].set_linewidth(1.5)
        ax.spines['bottom'].set_linewidth(1.5)

        # 旋转x轴标签
        plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)

    # 计算图例宽度
    legend_width = 0.15  # 默认值

    # 在图表右侧外部添加统一的图例
    if all_legend_handles and all_legend_labels:
        # 使用第一个子图来放置图例,但位置在图表外部
        first_ax = axes[0]
        legend = first_ax.legend(all_legend_handles, all_legend_labels,
                               bbox_to_anchor=(1.05, 1),  # 位置在右侧外部
                               loc='upper left',           # 图例内部对齐方式
                               fontsize=10,
                               frameon=True,
                               fancybox=True,
                               shadow=True,
                               framealpha=0.95,
                               borderpad=1,                # 图例内边距
                               columnspacing=1,            # 列间距
                               handlelength=2,             # 图例标记长度
                               handletextpad=0.5)          # 标记和文本间距

        legend.get_frame().set_facecolor('white')
        legend.get_frame().set_edgecolor('gray')
        legend.get_frame().set_linewidth(1)

        # 估算图例宽度
        legend_width = min(0.2, max(0.12, len(max(all_legend_labels, key=len)) * 0.012))

    # 设置总标题
    if args.title:
        title = args.title
    else:
        title = f"multiple"

    fig.suptitle(title, fontsize=18, fontweight='bold')

    # 调整子图布局,为右侧图例留出空间
    plt.tight_layout()
    plt.subplots_adjust(right=0.85)  # 右侧留出15%的空间给图例

    # 在整个图形上添加节点标注（只显示一次）
    add_node_annotations(fig, notes_dict, legend_width)

    # 处理输出
    save_plot(fig, title, args)

def print_args():
    """打印命令行参数"""
    print("\n命令行参数:")
    for k, v in vars(args).items():
        print(f"{k}: {v}")
        print("-"*50)
    
def save_plot(fig, title, args):
    """保存或显示图表"""
    if args.output:
        if os.path.isdir(args.output):
            safe_title = "".join(c for c in title if c.isalnum() or c in " _-")
            output_path = os.path.join(args.output, f"{safe_title}.png")
        else:
            output_path = args.output

        # 注意：这里不再调用 plt.tight_layout(),因为已经在上面调整过了
        fig.savefig(output_path, dpi=300, bbox_inches='tight')
        print(f"图表已保存至: {output_path}")
    else:
        plt.show()

def plot_data(args):
    """主绘图函数 - 根据输入文件数量选择绘图模式"""
    if len(args.inputs) == 1:
        print("single file mode")
        plot_single_file(args)
    else:
        print("multiple files mode")
        plot_multiple_files(args)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="CSV数据多轴折线图绘制工具",
                                   epilog="""
使用示例:
单文件模式:
python line_plotter.py data.csv -x minute -xf '%Y.%m.%d_%H.%M.%S' -y field1 field2 field3
python line_plotter.py data.csv -y temperature humidity pressure --labels temperature:温度 humidity:湿度 pressure:压力
python line_plotter.py data.csv -y VST1_T_sensor Y_std attenuation --labels minute:时间 VST1_T_sensor:传感器温度 Y_std:对比度 attenuation:衰减率

多文件模式:
python line_plotter.py data1.csv data2.csv data3.csv -f exp1 exp2 exp3 -x minute -y field1 field2 -s solid dashed dashdot
python line_plotter.py file1.csv file2.csv -f baseline experiment -x date -xd '%m-%d' -y sales profit --labels date:日期 sales:销售额 profit:利润 -t '多文件对比'

自定义标签示例:
python line_plotter.py data.csv -x time -y temp humidity --labels time:时间(小时) temp:温度(°C) humidity:相对湿度(%)
python line_plotter.py data.csv -y field1 field2 field3 --labels field1:数值1 field2:数值2 field3:数值3

水平线示例:
python line_plotter.py data.csv -y field1 field2 -l field1:40 field1:50:#FF0000 field1:60:#00FF00:"绿色警戒线" field2:30:"标准线"
python line_plotter.py data.csv -y VST1_T_sensor Y_std -l VST1_T_sensor:40 VST1_T_sensor:60:#FF0000:"红色高温警告" Y_std:35:#0000FF:"蓝色基准线"

节点标注示例（支持换行）:
python line_plotter.py data.csv -y field1 field2 -n "默认消息" -n lt:"左上角\\n第二行" -n rt:#FF0000:"红色右上角\\n支持多行\\n文本显示"

坐标轴范围设置示例:
# 数字格式的X轴范围
python line_plotter.py data.csv -y field1 field2 --xlim 0:100 --ylim field1:0:50 field2:10:90

# 日期格式的X轴范围
python line_plotter.py data.csv -x minute -xf '%Y.%m.%d_%H.%M.%S' -y temp humidity --xlim "2025.08.04_15.48.09:2025.08.04_16.48.09" --ylim temp:20:40 humidity:30:80

# 多字段Y轴范围设置
python line_plotter.py data.csv -y VST1_T_sensor attenuation Y_std --ylim VST1_T_sensor:40:70 attenuation:0.8:1.0 Y_std:30:50

# 多文件模式
python line_plotter.py file1.csv file2.csv -f exp1 exp2 -y field1 field2 --xlim 0:200 --ylim field1:0:100 field2:-10:10

""")
    parser.add_argument('inputs', nargs='+', help='输入CSV文件路径(支持多个文件)')
    parser.add_argument('-x', type=str, default='minute', help='横坐标字段名')
    parser.add_argument('-y', type=str, nargs='+', default=[], help='要绘制的数据字段名列表')
    parser.add_argument('-xf', type=str, default="%Y.%m.%d_%H.%M.%S", help="横坐标数据格式(如:%%Y.%%m.%%d_%%H.%%M.%%S)")
    parser.add_argument('-xd', type=str, default="%H.%M.%S", help="横坐标显示格式(如:%%H.%%M.%%S)")
    parser.add_argument('-xt', '--x_num_ticks', type=int, default=10, help='横坐标刻度间隔(数字)或刻度数量')
    parser.add_argument('-t', '--title', type=str, help='图表标题')
    parser.add_argument('-f','--flags', type=str, nargs='*', help='多文件模式下的标识符列表(与inputs一一对应)')
    parser.add_argument('-s','--line_styles', type=str, nargs='*', choices=['solid', 'dashed', 'dashdot', 'dotted'], help='多文件模式下的线形列表(与inputs一一对应)')
    parser.add_argument('-l','--hlines', type=str, nargs='*',
                       help='水平参考线设置。格式: field:value[:color][:desc]，其中field和value必填，color和desc可选。'
                            '颜色格式为十六进制(如#FF0000)，通过#开头识别。'
                            '例如: field1:40 field1:50:#FF0000 field1:60:#00FF00:"绿色警戒线" field1:70:"普通警戒线"')

    parser.add_argument('-c','--colors', type=int, nargs='*', help='颜色索引列表(与inputs一一对应),从color_list中选择颜色。例如: -c 0 3 5 表示使用第0、3、5个颜色')
    parser.add_argument('-n', '--notes', type=str, nargs='*', help='节点标注设置。格式: "缺省消息" 或 position:"消息" 或 position:color:"消息"。'
                        '位置选项: lt(左上), rt(右上), rb(右下), lb(左下), lc(左中), rc(右中), ct(中上), cb(中下)。支持\\n换行符。'
                        '例如: -n "默认消息" -n lt:"左上角\\n第二行" -n rt:#FF0000:"红色右上角" -n lc:"左中位置"')
    parser.add_argument('--labels', type=str, nargs='*',
                       help='坐标轴标签替换。格式: field:label。例如: minute:时间 VST1_T_sensor:sensor温度 Y_std:对比度 attenuation:衰减率')
    
    # 修改后的坐标轴范围参数
    parser.add_argument('--xlim', type=str, 
                       help='X轴数据范围设置。格式: start:end，支持数字和日期。'
                            '数字示例: --xlim 0:100；'
                            '日期示例: --xlim "2025.08.04_15.48.09:2025.08.04_16.48.09" (需配合-xf使用)')
    
    parser.add_argument('--ylim', type=str, nargs='*', 
                       help='Y轴数据范围设置。格式: field:start:end。'
                            '例如: --ylim VST1_T_sensor:40:70 attenuation:0.8:1.0')
    
    parser.add_argument('-v', '--verbose', action='store_true', help='详细模式')
    parser.add_argument('-o', '--output', help='图表输出路径(文件或目录)')

    args = parser.parse_args()
    if args.xf and args.xf.lower() == 'none':
        args.xf = None
    if args.xd and args.xd.lower() == 'none':
        args.xd = None
    plot_data(args)
